// Datapumps for communicating with Flickr
// Copyright 2006 (C) Ralph Thomas

#include <apps/photo/flickr_pumps.h>
#include <apps/photo/flickr.h>
#include <binder.hpp>
#include <adobe/name.hpp>
#include <boost/shared_ptr.hpp>
#include <toolkit/application.h>
#include <datapumps/progress.h>
#include <util/thread.h>
#include <util/open.h>
#include <util/registry.h>
#include <binder.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/signal.hpp>
#include <util/concepts/list.h>
#include <iostream>

namespace tk = toolkit;
namespace bfs = boost::filesystem;

namespace flickr {
	//
	/// This struct is created as a singleton. All of the flickr
	/// datapumps use it.
	//
	struct flickr_state_t {
		enum state_t {
			kConnecting,
			kDisconnected,
			kUnauthenticated,
			kAuthenticating,
			kAuthenticated,
			kUploading
		};
		typedef boost::function<void (state_t)>					state_function_t;
		typedef boost::function<void (unsigned int, const std::string&)>	upload_function_t;
		typedef boost::function<void (const std::string&)>			auth_function_t;
		typedef boost::signal<void (state_t)>					state_signal_t;
		typedef boost::signals::scoped_connection				state_connection_t;
		typedef boost::shared_ptr<flickr_state_t>				flickr_state_ptr_t;

		static flickr_state_ptr_t getInstance() {
			//
			// Eventually use a weak ptr for our own reference and let
			// the rest be refcounted.
			//
			static flickr_state_ptr_t instance;
			if( !instance ) instance = flickr_state_ptr_t( new flickr_state_t );
			return instance;
		}
		
		boost::signals::connection subscribe_state( const state_function_t& fn ) {
			boost::signals::connection connection( state_signal_m.connect( fn ) );
			state_signal_m( state_m );
			return connection;
		}
		state_t get_state() const { return state_m; }
		//
		/// Connect to flickr and get the URL that must be visited to
		/// complete authentication.
		///
		/// \param	fn	the function to call with the
		///			authentication URL.
		//
		void connect( const auth_function_t& fn ) {
			if( state_m == kConnecting ) return;
			set_state( kConnecting );
			auth_function_m = fn;
			thread_m.adoptWork( boost::bind( &flickr_state_t::do_connect, this ) );
		}
		//
		/// Complete the authentication process with flickr and get a
		/// token. This is done once the user has completed the login
		/// process at flickr.com.
		//
		void authenticate() {
			if( state_m != kUnauthenticated ) return;
			set_state( kAuthenticating );
			thread_m.adoptWork( boost::bind( &flickr_state_t::do_authenticate, this, frob_m ) );
		}
		//
		/// Upload the given array of photos. Each entry in the array
		/// should be a dictionary containing the "url", "tags", "title",
		/// "description", etc.
		///
		/// \param	photos		the photos to upload.
		/// \param	progress	the function to call with progress information.
		//
		void upload( const adobe::any_regular_t& photos ) {
			//
			// Allow multiple uploads to be queued.
			//
			if( state_m != kAuthenticated && state_m != kUploading ) return;
			set_state( kUploading );
			thread_m.adoptWork( boost::bind( &flickr_state_t::do_upload, this, token_m, photos ) );
		}
	  private:
		util::thread		thread_m;
		std::string		token_m;
		state_t			state_m;
		state_signal_t		state_signal_m;
		//
		// Connect/Authentication.
		// 
		auth_function_t		auth_function_m;
		std::string		frob_m;

		flickr_state_t() : state_m( kDisconnected ) {
			//
			// See if we can get the token out of the registry.
			//
			adobe::any_regular_t token( util::registry_get( "/apps/mission-photo/flickr_token" ) );
			if( token == adobe::any_regular_t() ) return;
			if( token.type() != typeid( std::string ) ) return;
			token_m = token.cast<std::string>();
			state_m = kAuthenticated;
		}

		void set_state( state_t new_state ) {
			if( new_state == state_m ) return;
			state_m = new_state;
			state_signal_m( state_m );
		}
		//
		/// Contacts the flickr service and gets a frob. This is required
		/// for authentication.
		///
		/// \param	target	the pump to return the frob to.
		//
		static void do_connect( flickr_state_t* target ) {
			try {
				std::string frob = get_frob();
				tk::application::getInstance().executeInMain(
					boost::bind( &flickr_state_t::on_connect, target, frob ) );
			} catch( const std::exception& ex ) {
				tk::application::getInstance().executeInMain(
					boost::bind( &flickr_state_t::on_error, target, ex.what() ) );
			}
		}
		//
		/// Called (indirectly) by the above do_connect function.
		///
		/// \param	frob	the frob to use to authenticate with flickr.
		//
		void on_connect( std::string frob ) {
			frob_m = frob;
			if( auth_function_m ) auth_function_m( get_auth_url( frob ) );
			set_state( kUnauthenticated );
		}
		//
		/// Contacts the flickr service and attempts to get a token.
		///
		/// \param	target	the target to return the token to.
		//
		static void do_authenticate( flickr_state_t* target, std::string frob ) {
			try {
				std::string token = get_token( frob );
				tk::application::getInstance().executeInMain(
					boost::bind( &flickr_state_t::on_authenticate, target, token ) );
			} catch( const std::exception& ex ) {
				tk::application::getInstance().executeInMain(
					boost::bind( &flickr_state_t::on_error, target, ex.what() ) );
			}
		}
		//
		/// Called (indirectly) by the above do_authenticate function.
		///
		/// \param	token	the token to use when uploading.
		//
		void on_authenticate( std::string token ) {
			token_m = token;
			util::registry_set( "/apps/mission-photo/flickr_token", adobe::any_regular_t( token_m ) );
			set_state( kAuthenticated );
		}
		static inline std::string dict_get( const adobe::dictionary_t& dict, const char* name, const std::string& def ) {
			std::string oval;
			if( !dict.count( adobe::name_t( name ) ) ) return def;
			if( dict.get<adobe::any_regular_t>( adobe::name_t( name ) ).cast( oval ) ) return oval;
			return def;
		}
		static void update_progress(datapumps::progress_report_t& progress, double start, const std::string& url, double percentage) {
			progress.state_progress( (unsigned int) (start + percentage), url );
		}
		//
		/// Upload a single photo.
		//
		static void upload_one( const std::string& token, const adobe::any_regular_t& photo, int& count, int size, datapumps::progress_report_t& progress ) {
			count++;
			//
			// Pretty much everything is optional here. If we find an entry without
			// a url then we skip it.
			//
			std::string url;
			try {
				url = photo.cast<std::string>();
				if( url.empty() ) return;
				progress.state_progress((count-1) * 100, url);
			} catch( ... ) {
				progress.state_progress((count-1) * 100, url);
			}
			//
			// Just attempt to extract the path; if it's not a file URL then don't
			// bother.
			//
			std::string protocol( "file://" );
			if( url.find( protocol ) != 0 ) return;
			bfs::path file( url.substr( protocol.size() ), bfs::native );
			if( !bfs::exists( file ) ) return;
			bfs::filebuf fbuf;
			std::streambuf* sbuf = fbuf.open( file, std::ios_base::in | std::ios_base::binary );
		        if( !sbuf ) return;
			//
			// Finally try to send this photo.
			//
			upload_photo( token, "untitled", "", "", true, false, false, fbuf, boost::bind( &update_progress, boost::ref(progress),  (count - 1) * 100.0, url, _1 ) );
			//
			// Update the count.
			//
			if( size != 0 ) progress.state_progress(count * 100, url);
		}
		//
		/// Upload the array of photos.
		///
		/// \param	target		the target to call when done.
		/// \param	token		the token to upload with.
		/// \param	photos		the photos to upload
		/// \param	progress	the function to call (in the main thread) with progress.
		//
		static void do_upload( flickr_state_t* target, std::string token, adobe::any_regular_t photos ) {
			datapumps::progress_report_t progress( "Flickr Upload" );
			util::concept::List l(util::concept::get_list(photos));
			progress.set_count( l.size() * 100 );
			try {
				int count( 0 );
				for (unsigned int i = 0; i < l.size(); i++)
					upload_one(token, l.at(i), count, l.size(), progress);
			} catch( const std::exception& ex ) {
				tk::application::getInstance().executeInMain(
					boost::bind( &flickr_state_t::on_error, target, ex.what() ) );
			}
			tk::application::getInstance().executeInMain(
				boost::bind( &flickr_state_t::on_upload, target ) );
		}
		//
		/// Called by the above function (indirectly) when uploading is done.
		//
		void on_upload() {
			set_state( kAuthenticated );
		}
		//
		/// Called when an error occurs.
		//
		void on_error( std::string error ) {
			toolkit::application::error( error );
		}
	};

	class flickr_state_pump_t : public adobe::binder_t {
		adobe::sheet_t::monitor_value_t		value_monitor_m;
		flickr_state_t::state_connection_t	connection_m;
		flickr_state_t::flickr_state_ptr_t	ref_m;
		void update( flickr_state_t::state_t s ) {
			if( !value_monitor_m ) return;
			std::string sval;
			switch( s ) {
			  case flickr_state_t::kConnecting:
				sval = "connecting";
				break;
			  case flickr_state_t::kDisconnected:
				sval = "disconnected";
				break;
			  case flickr_state_t::kUnauthenticated:
				sval = "unauthenticated";
				break;
			  case flickr_state_t::kAuthenticating:
				sval = "authenticating";
				break;
			  case flickr_state_t::kAuthenticated:
				sval = "authenticated";
				break;
			  case flickr_state_t::kUploading:
				sval = "uploading";
				break;
			}
			value_monitor_m( adobe::any_regular_t( sval ) );
		}
	  public:
		flickr_state_pump_t() {}
		virtual ~flickr_state_pump_t() {}
		void set( const adobe::any_regular_t& ) {}
		void set_signals( const adobe::sheet_t::monitor_value_t& value_monitor, const adobe::sheet_t::monitor_active_t& monitor_active ) {
			value_monitor_m = value_monitor;
			if( monitor_active ) monitor_active( false );
			if( !ref_m ) {
				ref_m = flickr_state_t::getInstance();
				connection_m = ref_m->subscribe_state( boost::bind( &flickr_state_pump_t::update, this, _1 ) );
			} else update( ref_m->get_state() );
		}
		void substitute( const adobe::name_t&, const adobe::any_regular_t& ) {}
		void invoke() {}
		adobe::binder_ptr_t copy() const {
			return adobe::binder_ptr_t( new flickr_state_pump_t() );
		}
	};
	struct fn_t : public adobe::binder_t {
		typedef boost::function<void()> function_t;
		fn_t( const function_t& fn ) : fn_m( fn ) {}
		virtual ~fn_t() {}
		void set( const adobe::any_regular_t& ) {}
		void set_signals( const adobe::sheet_t::monitor_value_t&, const adobe::sheet_t::monitor_active_t& ) {}
		void substitute( const adobe::name_t&, const adobe::any_regular_t& ) {}
		void invoke() { fn_m(); }
		adobe::binder_ptr_t copy() const {
			return adobe::binder_ptr_t( new fn_t( fn_m ) );
		}
	  private:
		function_t fn_m;
	};
	static void connect_fn() {
		flickr_state_t::flickr_state_ptr_t ref_m( flickr_state_t::getInstance() );
		//
		// Open the URL when done.
		//
		ref_m->connect( &util::open_url );
	}
	static void auth_fn() {
		flickr_state_t::flickr_state_ptr_t ref_m( flickr_state_t::getInstance() );
		ref_m->authenticate();
	}
	adobe::any_regular_t make_flickr_state() {
		return adobe::any_regular_t( adobe::binder_ptr_t( new flickr_state_pump_t() ) );
	}
	adobe::any_regular_t make_flickr_connect() {
		return adobe::any_regular_t( adobe::binder_ptr_t( new fn_t( &connect_fn ) ) );
	}
	adobe::any_regular_t make_flickr_auth() {
		return adobe::any_regular_t( adobe::binder_ptr_t( new fn_t( &auth_fn ) ) );
	}
	class flickr_upload_pump_t : public adobe::binder_t {
		flickr_state_t::flickr_state_ptr_t	ref_m;
		adobe::any_regular_t						photos_m;
		adobe::binder_helper_t				photo_binder_m;
		adobe::dictionary_t					params_m;
		adobe::sheet_t&						sheet_m;
		void update_photos( const adobe::any_regular_t& val ) { photos_m = val; }
	  public:
		flickr_upload_pump_t( const adobe::dictionary_t& p, adobe::sheet_t& s )
		: photo_binder_m( s ), sheet_m( s ) {
			//
			// The photos item is mandatory.
			//
			photo_binder_m.initialize( p.at( adobe::static_name_t( "photos" ) ),
				boost::bind( &flickr_upload_pump_t::update_photos, this, _1 ), 0 );
			ref_m = flickr_state_t::getInstance();
		}
		virtual ~flickr_upload_pump_t() {}
		void set( const adobe::any_regular_t& ) {}
		void set_signals( const adobe::sheet_t::monitor_value_t&, const adobe::sheet_t::monitor_active_t& ) {}
		void substitute( const adobe::name_t& name, const adobe::any_regular_t& value ) {
			photo_binder_m.substitute( name, value );
		}
		void invoke() {
			ref_m->upload( photos_m );
		}
		adobe::binder_ptr_t copy() const {
			return adobe::binder_ptr_t( new flickr_upload_pump_t( params_m, sheet_m ) );
		}
	};
	adobe::any_regular_t make_flickr_upload( const adobe::dictionary_t& params, adobe::sheet_t& sheet ) {
		return adobe::any_regular_t( adobe::binder_ptr_t( new flickr_upload_pump_t( params, sheet ) ) );
	}

};
